/*
 * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.swing.table;

import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.util.Vector;
import java.util.Enumeration;
import java.util.EventListener;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.io.Serializable;
import sun.swing.SwingUtilities2;

/**
 * The standard column-handler for a <code>JTable</code>.
 * <p>
 * <strong>Warning:</strong>
 * Serialized objects of this class will not be compatible with
 * future Swing releases. The current serialization support is
 * appropriate for short term storage or RMI between applications running
 * the same version of Swing.  As of 1.4, support for long term storage
 * of all JavaBeans&trade;
 * has been added to the <code>java.beans</code> package.
 * Please see {@link java.beans.XMLEncoder}.
 *
 * @author Alan Chung
 * @author Philip Milne
 * @see JTable
 */
public class DefaultTableColumnModel implements TableColumnModel,
    PropertyChangeListener, ListSelectionListener, Serializable {
//
// Instance Variables
//

  /**
   * Array of TableColumn objects in this model
   */
  protected Vector<TableColumn> tableColumns;

  /**
   * Model for keeping track of column selections
   */
  protected ListSelectionModel selectionModel;

  /**
   * Width margin between each column
   */
  protected int columnMargin;

  /**
   * List of TableColumnModelListener
   */
  protected EventListenerList listenerList = new EventListenerList();

  /**
   * Change event (only one needed)
   */
  transient protected ChangeEvent changeEvent = null;

  /**
   * Column selection allowed in this column model
   */
  protected boolean columnSelectionAllowed;

  /**
   * A local cache of the combined width of all columns
   */
  protected int totalColumnWidth;

//
// Constructors
//

  /**
   * Creates a default table column model.
   */
  public DefaultTableColumnModel() {
    super();

    // Initialize local ivars to default
    tableColumns = new Vector<TableColumn>();
    setSelectionModel(createSelectionModel());
    setColumnMargin(1);
    invalidateWidthCache();
    setColumnSelectionAllowed(false);
  }

//
// Modifying the model
//

  /**
   * Appends <code>aColumn</code> to the end of the
   * <code>tableColumns</code> array.
   * This method also posts the <code>columnAdded</code>
   * event to its listeners.
   *
   * @param aColumn the <code>TableColumn</code> to be added
   * @throws IllegalArgumentException if <code>aColumn</code> is <code>null</code>
   * @see #removeColumn
   */
  public void addColumn(TableColumn aColumn) {
    if (aColumn == null) {
      throw new IllegalArgumentException("Object is null");
    }

    tableColumns.addElement(aColumn);
    aColumn.addPropertyChangeListener(this);
    invalidateWidthCache();

    // Post columnAdded event notification
    fireColumnAdded(new TableColumnModelEvent(this, 0,
        getColumnCount() - 1));
  }

  /**
   * Deletes the <code>column</code> from the
   * <code>tableColumns</code> array.  This method will do nothing if
   * <code>column</code> is not in the table's columns list.
   * <code>tile</code> is called
   * to resize both the header and table views.
   * This method also posts a <code>columnRemoved</code>
   * event to its listeners.
   *
   * @param column the <code>TableColumn</code> to be removed
   * @see #addColumn
   */
  public void removeColumn(TableColumn column) {
    int columnIndex = tableColumns.indexOf(column);

    if (columnIndex != -1) {
      // Adjust for the selection
      if (selectionModel != null) {
        selectionModel.removeIndexInterval(columnIndex, columnIndex);
      }

      column.removePropertyChangeListener(this);
      tableColumns.removeElementAt(columnIndex);
      invalidateWidthCache();

      // Post columnAdded event notification.  (JTable and JTableHeader
      // listens so they can adjust size and redraw)
      fireColumnRemoved(new TableColumnModelEvent(this,
          columnIndex, 0));
    }
  }

  /**
   * Moves the column and heading at <code>columnIndex</code> to
   * <code>newIndex</code>.  The old column at <code>columnIndex</code>
   * will now be found at <code>newIndex</code>.  The column
   * that used to be at <code>newIndex</code> is shifted
   * left or right to make room.  This will not move any columns if
   * <code>columnIndex</code> equals <code>newIndex</code>.  This method
   * also posts a <code>columnMoved</code> event to its listeners.
   *
   * @param columnIndex the index of column to be moved
   * @param newIndex new index to move the column
   * @throws IllegalArgumentException if <code>column</code> or <code>newIndex</code> are not in the
   * valid range
   */
  public void moveColumn(int columnIndex, int newIndex) {
    if ((columnIndex < 0) || (columnIndex >= getColumnCount()) ||
        (newIndex < 0) || (newIndex >= getColumnCount())) {
      throw new IllegalArgumentException("moveColumn() - Index out of range");
    }

    TableColumn aColumn;

    // If the column has not yet moved far enough to change positions
    // post the event anyway, the "draggedDistance" property of the
    // tableHeader will say how far the column has been dragged.
    // Here we are really trying to get the best out of an
    // API that could do with some rethinking. We preserve backward
    // compatibility by slightly bending the meaning of these methods.
    if (columnIndex == newIndex) {
      fireColumnMoved(new TableColumnModelEvent(this, columnIndex, newIndex));
      return;
    }
    aColumn = tableColumns.elementAt(columnIndex);

    tableColumns.removeElementAt(columnIndex);
    boolean selected = selectionModel.isSelectedIndex(columnIndex);
    selectionModel.removeIndexInterval(columnIndex, columnIndex);

    tableColumns.insertElementAt(aColumn, newIndex);
    selectionModel.insertIndexInterval(newIndex, 1, true);
    if (selected) {
      selectionModel.addSelectionInterval(newIndex, newIndex);
    } else {
      selectionModel.removeSelectionInterval(newIndex, newIndex);
    }

    fireColumnMoved(new TableColumnModelEvent(this, columnIndex,
        newIndex));
  }

  /**
   * Sets the column margin to <code>newMargin</code>.  This method
   * also posts a <code>columnMarginChanged</code> event to its
   * listeners.
   *
   * @param newMargin the new margin width, in pixels
   * @see #getColumnMargin
   * @see #getTotalColumnWidth
   */
  public void setColumnMargin(int newMargin) {
    if (newMargin != columnMargin) {
      columnMargin = newMargin;
      // Post columnMarginChanged event notification.
      fireColumnMarginChanged();
    }
  }

//
// Querying the model
//

  /**
   * Returns the number of columns in the <code>tableColumns</code> array.
   *
   * @return the number of columns in the <code>tableColumns</code> array
   * @see #getColumns
   */
  public int getColumnCount() {
    return tableColumns.size();
  }

  /**
   * Returns an <code>Enumeration</code> of all the columns in the model.
   *
   * @return an <code>Enumeration</code> of the columns in the model
   */
  public Enumeration<TableColumn> getColumns() {
    return tableColumns.elements();
  }

  /**
   * Returns the index of the first column in the <code>tableColumns</code>
   * array whose identifier is equal to <code>identifier</code>,
   * when compared using <code>equals</code>.
   *
   * @param identifier the identifier object
   * @return the index of the first column in the <code>tableColumns</code> array whose identifier
   * is equal to <code>identifier</code>
   * @throws IllegalArgumentException if <code>identifier</code> is <code>null</code>, or if no
   * <code>TableColumn</code> has this <code>identifier</code>
   * @see #getColumn
   */
  public int getColumnIndex(Object identifier) {
    if (identifier == null) {
      throw new IllegalArgumentException("Identifier is null");
    }

    Enumeration enumeration = getColumns();
    TableColumn aColumn;
    int index = 0;

    while (enumeration.hasMoreElements()) {
      aColumn = (TableColumn) enumeration.nextElement();
      // Compare them this way in case the column's identifier is null.
      if (identifier.equals(aColumn.getIdentifier())) {
        return index;
      }
      index++;
    }
    throw new IllegalArgumentException("Identifier not found");
  }

  /**
   * Returns the <code>TableColumn</code> object for the column
   * at <code>columnIndex</code>.
   *
   * @param columnIndex the index of the column desired
   * @return the <code>TableColumn</code> object for the column at <code>columnIndex</code>
   */
  public TableColumn getColumn(int columnIndex) {
    return tableColumns.elementAt(columnIndex);
  }

  /**
   * Returns the width margin for <code>TableColumn</code>.
   * The default <code>columnMargin</code> is 1.
   *
   * @return the maximum width for the <code>TableColumn</code>
   * @see #setColumnMargin
   */
  public int getColumnMargin() {
    return columnMargin;
  }

  /**
   * Returns the index of the column that lies at position <code>x</code>,
   * or -1 if no column covers this point.
   *
   * In keeping with Swing's separable model architecture, a
   * TableColumnModel does not know how the table columns actually appear on
   * screen.  The visual presentation of the columns is the responsibility
   * of the view/controller object using this model (typically JTable).  The
   * view/controller need not display the columns sequentially from left to
   * right.  For example, columns could be displayed from right to left to
   * accommodate a locale preference or some columns might be hidden at the
   * request of the user.  Because the model does not know how the columns
   * are laid out on screen, the given <code>xPosition</code> should not be
   * considered to be a coordinate in 2D graphics space.  Instead, it should
   * be considered to be a width from the start of the first column in the
   * model.  If the column index for a given X coordinate in 2D space is
   * required, <code>JTable.columnAtPoint</code> can be used instead.
   *
   * @param x the horizontal location of interest
   * @return the index of the column or -1 if no column is found
   * @see javax.swing.JTable#columnAtPoint
   */
  public int getColumnIndexAtX(int x) {
    if (x < 0) {
      return -1;
    }
    int cc = getColumnCount();
    for (int column = 0; column < cc; column++) {
      x = x - getColumn(column).getWidth();
      if (x < 0) {
        return column;
      }
    }
    return -1;
  }

  /**
   * Returns the total combined width of all columns.
   *
   * @return the <code>totalColumnWidth</code> property
   */
  public int getTotalColumnWidth() {
    if (totalColumnWidth == -1) {
      recalcWidthCache();
    }
    return totalColumnWidth;
  }

//
// Selection model
//

  /**
   * Sets the selection model for this <code>TableColumnModel</code>
   * to <code>newModel</code>
   * and registers for listener notifications from the new selection
   * model.  If <code>newModel</code> is <code>null</code>,
   * an exception is thrown.
   *
   * @param newModel the new selection model
   * @throws IllegalArgumentException if <code>newModel</code> is <code>null</code>
   * @see #getSelectionModel
   */
  public void setSelectionModel(ListSelectionModel newModel) {
    if (newModel == null) {
      throw new IllegalArgumentException("Cannot set a null SelectionModel");
    }

    ListSelectionModel oldModel = selectionModel;

    if (newModel != oldModel) {
      if (oldModel != null) {
        oldModel.removeListSelectionListener(this);
      }

      selectionModel = newModel;
      newModel.addListSelectionListener(this);
    }
  }

  /**
   * Returns the <code>ListSelectionModel</code> that is used to
   * maintain column selection state.
   *
   * @return the object that provides column selection state.  Or <code>null</code> if row selection
   * is not allowed.
   * @see #setSelectionModel
   */
  public ListSelectionModel getSelectionModel() {
    return selectionModel;
  }

  // implements javax.swing.table.TableColumnModel

  /**
   * Sets whether column selection is allowed.  The default is false.
   *
   * @param flag true if column selection will be allowed, false otherwise
   */
  public void setColumnSelectionAllowed(boolean flag) {
    columnSelectionAllowed = flag;
  }

  // implements javax.swing.table.TableColumnModel

  /**
   * Returns true if column selection is allowed, otherwise false.
   * The default is false.
   *
   * @return the <code>columnSelectionAllowed</code> property
   */
  public boolean getColumnSelectionAllowed() {
    return columnSelectionAllowed;
  }

  // implements javax.swing.table.TableColumnModel

  /**
   * Returns an array of selected columns.  If <code>selectionModel</code>
   * is <code>null</code>, returns an empty array.
   *
   * @return an array of selected columns or an empty array if nothing is selected or the
   * <code>selectionModel</code> is <code>null</code>
   */
  public int[] getSelectedColumns() {
    if (selectionModel != null) {
      int iMin = selectionModel.getMinSelectionIndex();
      int iMax = selectionModel.getMaxSelectionIndex();

      if ((iMin == -1) || (iMax == -1)) {
        return new int[0];
      }

      int[] rvTmp = new int[1 + (iMax - iMin)];
      int n = 0;
      for (int i = iMin; i <= iMax; i++) {
        if (selectionModel.isSelectedIndex(i)) {
          rvTmp[n++] = i;
        }
      }
      int[] rv = new int[n];
      System.arraycopy(rvTmp, 0, rv, 0, n);
      return rv;
    }
    return new int[0];
  }

  // implements javax.swing.table.TableColumnModel

  /**
   * Returns the number of columns selected.
   *
   * @return the number of columns selected
   */
  public int getSelectedColumnCount() {
    if (selectionModel != null) {
      int iMin = selectionModel.getMinSelectionIndex();
      int iMax = selectionModel.getMaxSelectionIndex();
      int count = 0;

      for (int i = iMin; i <= iMax; i++) {
        if (selectionModel.isSelectedIndex(i)) {
          count++;
        }
      }
      return count;
    }
    return 0;
  }

//
// Listener Support Methods
//

  // implements javax.swing.table.TableColumnModel

  /**
   * Adds a listener for table column model events.
   *
   * @param x a <code>TableColumnModelListener</code> object
   */
  public void addColumnModelListener(TableColumnModelListener x) {
    listenerList.add(TableColumnModelListener.class, x);
  }

  // implements javax.swing.table.TableColumnModel

  /**
   * Removes a listener for table column model events.
   *
   * @param x a <code>TableColumnModelListener</code> object
   */
  public void removeColumnModelListener(TableColumnModelListener x) {
    listenerList.remove(TableColumnModelListener.class, x);
  }

  /**
   * Returns an array of all the column model listeners
   * registered on this model.
   *
   * @return all of this default table column model's <code>ColumnModelListener</code>s or an empty
   * array if no column model listeners are currently registered
   * @see #addColumnModelListener
   * @see #removeColumnModelListener
   * @since 1.4
   */
  public TableColumnModelListener[] getColumnModelListeners() {
    return listenerList.getListeners(TableColumnModelListener.class);
  }

//
//   Event firing methods
//

  /**
   * Notifies all listeners that have registered interest for
   * notification on this event type.  The event instance
   * is lazily created using the parameters passed into
   * the fire method.
   *
   * @param e the event received
   * @see EventListenerList
   */
  protected void fireColumnAdded(TableColumnModelEvent e) {
    // Guaranteed to return a non-null array
    Object[] listeners = listenerList.getListenerList();
    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length - 2; i >= 0; i -= 2) {
      if (listeners[i] == TableColumnModelListener.class) {
        // Lazily create the event:
        // if (e == null)
        //  e = new ChangeEvent(this);
        ((TableColumnModelListener) listeners[i + 1]).
            columnAdded(e);
      }
    }
  }

  /**
   * Notifies all listeners that have registered interest for
   * notification on this event type.  The event instance
   * is lazily created using the parameters passed into
   * the fire method.
   *
   * @param e the event received
   * @see EventListenerList
   */
  protected void fireColumnRemoved(TableColumnModelEvent e) {
    // Guaranteed to return a non-null array
    Object[] listeners = listenerList.getListenerList();
    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length - 2; i >= 0; i -= 2) {
      if (listeners[i] == TableColumnModelListener.class) {
        // Lazily create the event:
        // if (e == null)
        //  e = new ChangeEvent(this);
        ((TableColumnModelListener) listeners[i + 1]).
            columnRemoved(e);
      }
    }
  }

  /**
   * Notifies all listeners that have registered interest for
   * notification on this event type.  The event instance
   * is lazily created using the parameters passed into
   * the fire method.
   *
   * @param e the event received
   * @see EventListenerList
   */
  protected void fireColumnMoved(TableColumnModelEvent e) {
    // Guaranteed to return a non-null array
    Object[] listeners = listenerList.getListenerList();
    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length - 2; i >= 0; i -= 2) {
      if (listeners[i] == TableColumnModelListener.class) {
        // Lazily create the event:
        // if (e == null)
        //  e = new ChangeEvent(this);
        ((TableColumnModelListener) listeners[i + 1]).
            columnMoved(e);
      }
    }
  }

  /**
   * Notifies all listeners that have registered interest for
   * notification on this event type.  The event instance
   * is lazily created using the parameters passed into
   * the fire method.
   *
   * @param e the event received
   * @see EventListenerList
   */
  protected void fireColumnSelectionChanged(ListSelectionEvent e) {
    // Guaranteed to return a non-null array
    Object[] listeners = listenerList.getListenerList();
    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length - 2; i >= 0; i -= 2) {
      if (listeners[i] == TableColumnModelListener.class) {
        // Lazily create the event:
        // if (e == null)
        //  e = new ChangeEvent(this);
        ((TableColumnModelListener) listeners[i + 1]).
            columnSelectionChanged(e);
      }
    }
  }

  /**
   * Notifies all listeners that have registered interest for
   * notification on this event type.  The event instance
   * is lazily created using the parameters passed into
   * the fire method.
   *
   * @see EventListenerList
   */
  protected void fireColumnMarginChanged() {
    // Guaranteed to return a non-null array
    Object[] listeners = listenerList.getListenerList();
    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length - 2; i >= 0; i -= 2) {
      if (listeners[i] == TableColumnModelListener.class) {
        // Lazily create the event:
        if (changeEvent == null) {
          changeEvent = new ChangeEvent(this);
        }
        ((TableColumnModelListener) listeners[i + 1]).
            columnMarginChanged(changeEvent);
      }
    }
  }

  /**
   * Returns an array of all the objects currently registered as <code><em>Foo</em>Listener</code>s
   * upon this model. <code><em>Foo</em>Listener</code>s are registered using the
   * <code>add<em>Foo</em>Listener</code> method.
   *
   * <p>
   *
   * You can specify the <code>listenerType</code> argument with a class literal, such as
   * <code><em>Foo</em>Listener.class</code>. For example, you can query a
   * <code>DefaultTableColumnModel</code> <code>m</code> for its column model listeners with the
   * following code:
   *
   * <pre>ColumnModelListener[] cmls = (ColumnModelListener[])(m.getListeners(ColumnModelListener.class));</pre>
   *
   * If no such listeners exist, this method returns an empty array.
   *
   * @param listenerType the type of listeners requested; this parameter should specify an interface
   * that descends from <code>java.util.EventListener</code>
   * @return an array of all objects registered as <code><em>Foo</em>Listener</code>s on this model,
   * or an empty array if no such listeners have been added
   * @throws ClassCastException if <code>listenerType</code> doesn't specify a class or interface
   * that implements <code>java.util.EventListener</code>
   * @see #getColumnModelListeners
   * @since 1.3
   */
  public <T extends EventListener> T[] getListeners(Class<T> listenerType) {
    return listenerList.getListeners(listenerType);
  }

//
// Implementing the PropertyChangeListener interface
//

  // PENDING(alan)
  // implements java.beans.PropertyChangeListener

  /**
   * Property Change Listener change method.  Used to track changes
   * to the column width or preferred column width.
   *
   * @param evt <code>PropertyChangeEvent</code>
   */
  public void propertyChange(PropertyChangeEvent evt) {
    String name = evt.getPropertyName();

    if (name == "width" || name == "preferredWidth") {
      invalidateWidthCache();
      // This is a misnomer, we're using this method
      // simply to cause a relayout.
      fireColumnMarginChanged();
    }

  }

//
// Implementing ListSelectionListener interface
//

  // implements javax.swing.event.ListSelectionListener

  /**
   * A <code>ListSelectionListener</code> that forwards
   * <code>ListSelectionEvents</code> when there is a column
   * selection change.
   *
   * @param e the change event
   */
  public void valueChanged(ListSelectionEvent e) {
    fireColumnSelectionChanged(e);
  }

//
// Protected Methods
//

  /**
   * Creates a new default list selection model.
   */
  protected ListSelectionModel createSelectionModel() {
    return new DefaultListSelectionModel();
  }

  /**
   * Recalculates the total combined width of all columns.  Updates the
   * <code>totalColumnWidth</code> property.
   */
  protected void recalcWidthCache() {
    Enumeration enumeration = getColumns();
    totalColumnWidth = 0;
    while (enumeration.hasMoreElements()) {
      totalColumnWidth += ((TableColumn) enumeration.nextElement()).getWidth();
    }
  }

  private void invalidateWidthCache() {
    totalColumnWidth = -1;
  }

} // End of class DefaultTableColumnModel
